{
"cells": [
{
"cell_type": "markdown",
"id": "b4c1a9e0",
"metadata": {},
"source": [
"# Modeling Slippage\n",
"\n",
"In live trading, orders rarely fill at the exact price a backtest assumes. Factors like spreads, latency, and the market impact of your own order push fill prices in an adverse direction. This difference is called *slippage*. A backtest that ignores it will overstate a strategy's true performance.\n",
"\n",
"This notebook demonstrates **PyBroker's** three built-in slippage models added in v2. It also introduces the [SlippageModel](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageModel) base class, which you can use to write your own custom models."
]
},
{
"cell_type": "markdown",
"id": "7d2e8f31",
"metadata": {},
"source": [
"## A Baseline Strategy\n",
"\n",
"To see the effect of each model, we reuse the dip-buying strategy from [Backtesting a Strategy](https://www.pybroker.com/en/latest/notebooks/2.%20Backtesting%20a%20Strategy.html). The rule is simple: buy when the latest close drops below the previous day's low. We allocate 25% of the portfolio to the position with [calc_target_shares](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.ExecContext.calc_target_shares) and hold it for 3 bars via [hold_bars](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.ExecContext.hold_bars). Because this strategy trades frequently, small per-fill costs will compound into a noticeable difference in total return."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "5a913c72",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:03.171242Z",
"iopub.status.busy": "2026-08-11T20:31:03.171153Z",
"iopub.status.idle": "2026-08-11T20:31:03.843725Z",
"shell.execute_reply": "2026-08-11T20:31:03.843010Z"
}
},
"outputs": [],
"source": [
"import pybroker\n",
"from pybroker import Strategy, YFinance\n",
"\n",
"pybroker.enable_data_source_cache(\"slippage\")\n",
"\n",
"\n",
"def buy_low(ctx):\n",
" # If shares were already purchased and are currently being held, then\n",
" # return.\n",
" if ctx.long_pos():\n",
" return\n",
" # If the latest close price is less than the previous day's low price,\n",
" # then place a buy order.\n",
" if ctx.bars >= 2 and ctx.close[-1] < ctx.low[-2]:\n",
" # Buy a number of shares that is equal to 25% of the portfolio.\n",
" ctx.buy_shares = ctx.calc_target_shares(0.25)\n",
" # Hold the position for 3 bars before liquidating.\n",
" ctx.hold_bars = 3\n",
"\n",
"\n",
"symbols = [\"F\", \"BAC\", \"T\"]\n",
"strategy = Strategy(YFinance(), start_date=\"1/1/2021\", end_date=\"1/1/2026\")\n",
"strategy.add_execution(buy_low, symbols)"
]
},
{
"cell_type": "markdown",
"id": "9e6b0d44",
"metadata": {},
"source": [
"Now, we run the baseline backtest with no slippage. Every order fills at the the midpoint between the bar's low and high prices ([PriceType.MIDDLE](https://www.pybroker.com/en/latest/reference/pybroker.common.html#pybroker.common.PriceType.MIDDLE)) by default:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "1c8f4a25",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:03.845701Z",
"iopub.status.busy": "2026-08-11T20:31:03.845504Z",
"iopub.status.idle": "2026-08-11T20:31:04.730678Z",
"shell.execute_reply": "2026-08-11T20:31:04.730160Z"
},
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading bar data...\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"[ 0% ]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"[**********************67%******* ] 2 of 3 completed"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"[*********************100%***********************] 3 of 3 completed"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loaded bar data: 0:00:01 \n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test split: 2021-01-04 00:00:00 to 2025-12-31 00:00:00\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 1255)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;225;255;0m 67%\u001b[39m \u001b[38;2;225;255;0m(851 of 1255)\u001b[39m |############## | Elapsed Time: 0:00:00 ETA: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(1255 of 1255)\u001b[39m |####################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished backtest: 0:00:01\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Total return: 18.02%\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" type | \n",
" symbol | \n",
" date | \n",
" created | \n",
" order_type | \n",
" intent | \n",
" shares | \n",
" limit_price | \n",
" market_price | \n",
" fill_price | \n",
" fees | \n",
"
\n",
" \n",
" | id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1 | \n",
" buy | \n",
" BAC | \n",
" 2021-01-11 | \n",
" 2021-01-08 | \n",
" market | \n",
" buy_to_open | \n",
" 768 | \n",
" NaN | \n",
" 32.52 | \n",
" 32.52 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 2 | \n",
" buy | \n",
" T | \n",
" 2021-01-11 | \n",
" 2021-01-08 | \n",
" market | \n",
" buy_to_open | \n",
" 1140 | \n",
" NaN | \n",
" 21.79 | \n",
" 21.79 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 3 | \n",
" sell | \n",
" BAC | \n",
" 2021-01-14 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 768 | \n",
" NaN | \n",
" 33.89 | \n",
" 33.89 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" sell | \n",
" T | \n",
" 2021-01-14 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 1140 | \n",
" NaN | \n",
" 22.02 | \n",
" 22.02 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" buy | \n",
" BAC | \n",
" 2021-01-19 | \n",
" 2021-01-15 | \n",
" market | \n",
" buy_to_open | \n",
" 767 | \n",
" NaN | \n",
" 32.90 | \n",
" 32.90 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" type symbol date created order_type intent shares \\\n",
"id \n",
"1 buy BAC 2021-01-11 2021-01-08 market buy_to_open 768 \n",
"2 buy T 2021-01-11 2021-01-08 market buy_to_open 1140 \n",
"3 sell BAC 2021-01-14 NaT stop_bar sell_to_close 768 \n",
"4 sell T 2021-01-14 NaT stop_bar sell_to_close 1140 \n",
"5 buy BAC 2021-01-19 2021-01-15 market buy_to_open 767 \n",
"\n",
" limit_price market_price fill_price fees \n",
"id \n",
"1 NaN 32.52 32.52 0.0 \n",
"2 NaN 21.79 21.79 0.0 \n",
"3 NaN 33.89 33.89 0.0 \n",
"4 NaN 22.02 22.02 0.0 \n",
"5 NaN 32.90 32.90 0.0 "
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"result = strategy.backtest()\n",
"print(f\"Total return: {result.metrics.total_return_pct:.2f}%\")\n",
"result.orders.head()"
]
},
{
"cell_type": "markdown",
"id": "e2a75b16",
"metadata": {},
"source": [
"## Fixed Slippage\n",
"\n",
"The [FixedSlippageModel](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.FixedSlippageModel) applies a fixed, adverse price adjustment measured in basis points (where 1 basis point equals 0.01%). Buy prices are increased by `bps`, while sell prices are decreased. Passing `bps=0` disables the adjustment entirely.\n",
"\n",
"Because the remaining examples run several more backtests, we will also disable logging with [disable_logging](https://www.pybroker.com/en/latest/reference/pybroker.scope.html#pybroker.scope.disable_logging) to keep the output short. Next, we attach the model to a [Strategy](https://www.pybroker.com/en/latest/reference/pybroker.strategy.html#pybroker.strategy.Strategy) with [set_slippage_model](https://www.pybroker.com/en/latest/reference/pybroker.strategy.html#pybroker.strategy.Strategy.set_slippage_model):"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "0f3d6c87",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:04.732164Z",
"iopub.status.busy": "2026-08-11T20:31:04.732028Z",
"iopub.status.idle": "2026-08-11T20:31:04.836313Z",
"shell.execute_reply": "2026-08-11T20:31:04.835792Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Total return: -8.66%\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" type | \n",
" symbol | \n",
" date | \n",
" created | \n",
" order_type | \n",
" intent | \n",
" shares | \n",
" limit_price | \n",
" market_price | \n",
" fill_price | \n",
" fees | \n",
"
\n",
" \n",
" | id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 2 | \n",
" buy | \n",
" T | \n",
" 2021-01-11 | \n",
" 2021-01-08 | \n",
" market | \n",
" buy_to_open | \n",
" 1140 | \n",
" NaN | \n",
" 21.79 | \n",
" 21.81 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" sell | \n",
" T | \n",
" 2021-01-14 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 1140 | \n",
" NaN | \n",
" 22.02 | \n",
" 22.00 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 11 | \n",
" buy | \n",
" T | \n",
" 2021-01-29 | \n",
" 2021-01-28 | \n",
" market | \n",
" buy_to_open | \n",
" 1128 | \n",
" NaN | \n",
" 21.76 | \n",
" 21.78 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 14 | \n",
" sell | \n",
" T | \n",
" 2021-02-03 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 1128 | \n",
" NaN | \n",
" 21.58 | \n",
" 21.56 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 17 | \n",
" buy | \n",
" T | \n",
" 2021-02-09 | \n",
" 2021-02-08 | \n",
" market | \n",
" buy_to_open | \n",
" 1158 | \n",
" NaN | \n",
" 21.65 | \n",
" 21.67 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" type symbol date created order_type intent shares \\\n",
"id \n",
"2 buy T 2021-01-11 2021-01-08 market buy_to_open 1140 \n",
"4 sell T 2021-01-14 NaT stop_bar sell_to_close 1140 \n",
"11 buy T 2021-01-29 2021-01-28 market buy_to_open 1128 \n",
"14 sell T 2021-02-03 NaT stop_bar sell_to_close 1128 \n",
"17 buy T 2021-02-09 2021-02-08 market buy_to_open 1158 \n",
"\n",
" limit_price market_price fill_price fees \n",
"id \n",
"2 NaN 21.79 21.81 0.0 \n",
"4 NaN 22.02 22.00 0.0 \n",
"11 NaN 21.76 21.78 0.0 \n",
"14 NaN 21.58 21.56 0.0 \n",
"17 NaN 21.65 21.67 0.0 "
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from pybroker import FixedSlippageModel\n",
"\n",
"pybroker.disable_logging()\n",
"\n",
"strategy.set_slippage_model(FixedSlippageModel(bps=10))\n",
"result = strategy.backtest()\n",
"print(f\"Total return: {result.metrics.total_return_pct:.2f}%\")\n",
"result.orders[result.orders[\"symbol\"] == \"T\"].head()"
]
},
{
"cell_type": "markdown",
"id": "84c2d7a9",
"metadata": {},
"source": [
"## Volatility Slippage\n",
"\n",
"Because slippage tends to grow as volatility rises, the [VolatilitySlippageModel](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.VolatilitySlippageModel) ties its adverse price adjustment directly to market movement. It scales the slippage using the fill bar's [Average True Range (ATR)](https://en.wikipedia.org/wiki/Average_true_range) (see [atr](https://www.pybroker.com/en/latest/reference/pybroker.vect.html#pybroker.vect.atr)), moving the fill price against your order by `scale * ATR`. The ATR is calculated over the `atr_period` leading up to the fill bar, and any fills during the warmup period (before a full ATR window is established) remain unadjusted:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2d9a4e63",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:04.837902Z",
"iopub.status.busy": "2026-08-11T20:31:04.837770Z",
"iopub.status.idle": "2026-08-11T20:31:05.348062Z",
"shell.execute_reply": "2026-08-11T20:31:05.347507Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Total return: -39.55%\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" type | \n",
" symbol | \n",
" date | \n",
" created | \n",
" order_type | \n",
" intent | \n",
" shares | \n",
" limit_price | \n",
" market_price | \n",
" fill_price | \n",
" fees | \n",
"
\n",
" \n",
" | id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1 | \n",
" buy | \n",
" BAC | \n",
" 2021-01-11 | \n",
" 2021-01-08 | \n",
" market | \n",
" buy_to_open | \n",
" 768 | \n",
" NaN | \n",
" 32.52 | \n",
" 32.52 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 2 | \n",
" buy | \n",
" T | \n",
" 2021-01-11 | \n",
" 2021-01-08 | \n",
" market | \n",
" buy_to_open | \n",
" 1140 | \n",
" NaN | \n",
" 21.79 | \n",
" 21.79 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 3 | \n",
" sell | \n",
" BAC | \n",
" 2021-01-14 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 768 | \n",
" NaN | \n",
" 33.89 | \n",
" 33.89 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" sell | \n",
" T | \n",
" 2021-01-14 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 1140 | \n",
" NaN | \n",
" 22.02 | \n",
" 22.02 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" buy | \n",
" BAC | \n",
" 2021-01-19 | \n",
" 2021-01-15 | \n",
" market | \n",
" buy_to_open | \n",
" 767 | \n",
" NaN | \n",
" 32.90 | \n",
" 32.90 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" type symbol date created order_type intent shares \\\n",
"id \n",
"1 buy BAC 2021-01-11 2021-01-08 market buy_to_open 768 \n",
"2 buy T 2021-01-11 2021-01-08 market buy_to_open 1140 \n",
"3 sell BAC 2021-01-14 NaT stop_bar sell_to_close 768 \n",
"4 sell T 2021-01-14 NaT stop_bar sell_to_close 1140 \n",
"5 buy BAC 2021-01-19 2021-01-15 market buy_to_open 767 \n",
"\n",
" limit_price market_price fill_price fees \n",
"id \n",
"1 NaN 32.52 32.52 0.0 \n",
"2 NaN 21.79 21.79 0.0 \n",
"3 NaN 33.89 33.89 0.0 \n",
"4 NaN 22.02 22.02 0.0 \n",
"5 NaN 32.90 32.90 0.0 "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from pybroker import VolatilitySlippageModel\n",
"\n",
"strategy.set_slippage_model(VolatilitySlippageModel(atr_period=14, scale=0.1))\n",
"result = strategy.backtest()\n",
"print(f\"Total return: {result.metrics.total_return_pct:.2f}%\")\n",
"result.orders.head()"
]
},
{
"cell_type": "markdown",
"id": "c5e0b8f4",
"metadata": {},
"source": [
"## Volume Slippage\n",
"\n",
"The [VolumeSlippageModel](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.VolumeSlippageModel) accounts for limited market liquidity by applying two mechanics:\n",
"\n",
"1. **Volume limit:** The number of filled shares is capped at a percentage of the bar's total volume (`volume_limit * volume`). Any shares exceeding this limit are canceled rather than carried over to the next bar.\n",
"2. **Price impact:** The execution price moves against your order based on its size relative to the market. This adverse adjustment is calculated as `price_impact * (filled_shares / volume) ** 2` multiplied by the initial fill price.\n",
"\n",
"A $100,000 account will rarely hit these limits when trading liquid large caps. For example, a 25% allocation would just be a rounding error in Ford's daily volume. However, that same allocation can be a significant portion of the day's trading in thinly traded small caps. Without a volume model, the backtest unrealistically assumes the entire order fills at the quoted price:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "8e7f2a91",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:05.349748Z",
"iopub.status.busy": "2026-08-11T20:31:05.349653Z",
"iopub.status.idle": "2026-08-11T20:31:05.823995Z",
"shell.execute_reply": "2026-08-11T20:31:05.823472Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Return without a volume model: 16.67%\n",
"Return with a volume model: 12.46%\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" type | \n",
" symbol | \n",
" date | \n",
" created | \n",
" order_type | \n",
" intent | \n",
" shares | \n",
" limit_price | \n",
" market_price | \n",
" fill_price | \n",
" fees | \n",
"
\n",
" \n",
" | id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1 | \n",
" buy | \n",
" HURC | \n",
" 2021-01-06 | \n",
" 2021-01-05 | \n",
" market | \n",
" buy_to_open | \n",
" 874 | \n",
" NaN | \n",
" 30.30 | \n",
" 30.30 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 2 | \n",
" sell | \n",
" HURC | \n",
" 2021-01-11 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 874 | \n",
" NaN | \n",
" 30.28 | \n",
" 30.28 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 3 | \n",
" buy | \n",
" ESCA | \n",
" 2021-01-11 | \n",
" 2021-01-08 | \n",
" market | \n",
" buy_to_open | \n",
" 677 | \n",
" NaN | \n",
" 21.79 | \n",
" 21.79 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" buy | \n",
" BSET | \n",
" 2021-01-12 | \n",
" 2021-01-11 | \n",
" market | \n",
" buy_to_open | \n",
" 1268 | \n",
" NaN | \n",
" 20.39 | \n",
" 20.39 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" sell | \n",
" ESCA | \n",
" 2021-01-14 | \n",
" NaT | \n",
" stop_bar | \n",
" sell_to_close | \n",
" 677 | \n",
" NaN | \n",
" 22.85 | \n",
" 22.85 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" type symbol date created order_type intent shares \\\n",
"id \n",
"1 buy HURC 2021-01-06 2021-01-05 market buy_to_open 874 \n",
"2 sell HURC 2021-01-11 NaT stop_bar sell_to_close 874 \n",
"3 buy ESCA 2021-01-11 2021-01-08 market buy_to_open 677 \n",
"4 buy BSET 2021-01-12 2021-01-11 market buy_to_open 1268 \n",
"5 sell ESCA 2021-01-14 NaT stop_bar sell_to_close 677 \n",
"\n",
" limit_price market_price fill_price fees \n",
"id \n",
"1 NaN 30.30 30.30 0.0 \n",
"2 NaN 30.28 30.28 0.0 \n",
"3 NaN 21.79 21.79 0.0 \n",
"4 NaN 20.39 20.39 0.0 \n",
"5 NaN 22.85 22.85 0.0 "
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from pybroker import VolumeSlippageModel\n",
"\n",
"smallcaps = Strategy(YFinance(), start_date=\"1/1/2021\", end_date=\"1/1/2026\")\n",
"smallcaps.add_execution(buy_low, [\"ESCA\", \"BSET\", \"HURC\"])\n",
"result = smallcaps.backtest()\n",
"print(f\"Return without a volume model: {result.metrics.total_return_pct:.2f}%\")\n",
"\n",
"smallcaps.set_slippage_model(\n",
" VolumeSlippageModel(price_impact=0.1, volume_limit=0.025)\n",
")\n",
"result = smallcaps.backtest()\n",
"print(f\"Return with a volume model: {result.metrics.total_return_pct:.2f}%\")\n",
"result.orders.head()"
]
},
{
"cell_type": "markdown",
"id": "a9d4e5c8",
"metadata": {},
"source": [
"## Writing a Custom Slippage Model\n",
"\n",
"To create your own model, subclass [SlippageModel](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageModel) and override the [apply_slippage](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageModel.apply_slippage) method. This method takes a [SlippageContext](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageContext) object containing the order's [side](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageContext.side) (`\"buy\"` or `\"sell\"`), [symbol](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageContext.symbol), [shares](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageContext.shares), and the initial [fill_price](https://www.pybroker.com/en/latest/reference/pybroker.slippage.html#pybroker.slippage.SlippageContext.fill_price). Your method must then return a tuple with the adjusted `(shares, fill_price)`.\n",
"\n",
"The following example shows a model that applies a random amount of adverse slippage to every fill:\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "6f2b7d05",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:05.825620Z",
"iopub.status.busy": "2026-08-11T20:31:05.825520Z",
"iopub.status.idle": "2026-08-11T20:31:05.934866Z",
"shell.execute_reply": "2026-08-11T20:31:05.934281Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Total return: 3.89%\n"
]
}
],
"source": [
"from decimal import Decimal\n",
"\n",
"import numpy as np\n",
"from pybroker import SlippageContext, SlippageModel\n",
"\n",
"\n",
"class RandomSlippageModel(SlippageModel):\n",
" \"\"\"Applies random adverse slippage of up to ``max_bps`` per fill.\"\"\"\n",
"\n",
" def __init__(self, max_bps: float = 10, seed: int = 42):\n",
" self.max_bps = max_bps\n",
" self._rng = np.random.default_rng(seed)\n",
"\n",
" def apply_slippage(self, ctx: SlippageContext) -> tuple[Decimal, Decimal]:\n",
" bps = self._rng.uniform(0, self.max_bps)\n",
" adjustment = ctx.fill_price * Decimal(str(bps / 10_000))\n",
" if ctx.side == \"buy\":\n",
" fill_price = ctx.fill_price + adjustment\n",
" else:\n",
" fill_price = ctx.fill_price - adjustment\n",
" return ctx.shares, fill_price\n",
"\n",
"\n",
"strategy.set_slippage_model(RandomSlippageModel(max_bps=10, seed=42))\n",
"result = strategy.backtest()\n",
"print(f\"Total return: {result.metrics.total_return_pct:.2f}%\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}